#
# minst_t01
#
# [机器学习——训练MNIST数据集](https://blog.csdn.net/yasin_huang/article/details/112063808?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_baidulandingword~default-4-112063808-blog-103145455.pc_relevant_3mothn_strategy_and_data_recovery&spm=1001.2101.3001.4242.3&utm_relevant_index=7)
#
###############################################################################
#
from sklearn.metrics import confusion_matrix
from sklearn.model_selection import cross_val_predict
from sklearn.ensemble import VotingClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.svm import SVC
from sklearn.multiclass import OneVsRestClassifier
from sklearn.model_selection import cross_val_score
from sklearn.neighbors import KNeighborsClassifier
from sklearn.decomposition import PCA
import torch
import matplotlib.pyplot as plt
import mnist

# 首先，我们定义一些超参数
MNIST_ROOT = "e:/sfxData/DeepLearning"
EPOCHS = 20  # 总共训练批次
BATCH_SIZE = 512  # 大概需要2G的显存
DEVICE = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

# 装载数据
train_dataset = mnist.get_dataset(MNIST_ROOT, train=True)
X_train, y_train = train_dataset.data, train_dataset.targets
# X_test, y_test = mnist.load_test()

# print(X_train.shape)    # (60000, 28, 28)
# print(y_train.shape)    # (60000)
# print(X_train.size(0))  # 6000

# image = X_train[0]
# plt.imshow(image, cmap='binary', interpolation="nearest")
# plt.show()

# 降维
# 通过观察数据发现，每张图片的边缘存在大量空白，即使删掉这部分内容，也不会丢失太多信息。
# 针对这种情况，考虑使用降维来去除这些无关信息，降维还能够将两个相邻特征合并为一个，以此来降低维度，提升训练速度。

# 选择模型
# 本文将尝试使用几种模型对数据集进行训练：K邻近、SVM（支持向量机）、随机森林
# 其中，由于SVM是一个二元分类器，所以将会对10个数字分别训练10个分类器。
# 为了给模型找到合适的超参数，以更好的拟合数据，则使用GridSearchCV（网格搜索）对超参数进行微调。
# 最后采用模型集成来做预测。

# 评估模型
# 通过两种方式来评估模型性能，分别是交叉验证与混淆矩阵。

#
# 训练模型
#

# PCA
# 使用PCA（主成分分析）降维，问题在于如何设置合适的维度，n_components参数用于设置需要保留的主成分数；
# 若不确定其主成分数，可以设置值为[0.0, 1.0]，此时值表示要保留的方差率。


def pca():
    # 使用主成分分析降维，保留95%的可解释方差比
    pca = PCA(n_components=0.95)
    x = pca.fit_transform(X_train.reshape(X_train.size(0), -1))
    print(pca.n_components_)  # 输出154，即保留了154个主成分
    print(x.shape)  # (60000, 154)


# KNN
# KNN（K-邻近值）感觉是比较适合这项分类任务的模型。
# 使用sklearn提供的网格搜索，找出合适的模型超参数。
def knn():
    kn_clf = KNeighborsClassifier(n_neighbors=4, weights='distance', n_jobs=-1)

    x_train_ = X_train.reshape(X_train.size(0), -1)
    # 进行3次交叉验证
    scores = cross_val_score(kn_clf, x_train_, y_train, cv=3,
                             n_jobs=-1, scoring='accuracy')
    # 输出每次验证的正确率
    print(scores)
    # 输出平均正确率
    print(scores.mean())

    # 输出结果如下
    # [0.97330534 0.97069853 0.97154573]
    # 0.9718498685729129


# knn()

# SVM
# SVM是二元分类器，有一种策略是为每个数字训练一个二元分类器，比如数字“0”分类器，只判断是0与不是0；
# 针对该数据集则需要训练10个分类器。使用OneVsRestClassifier类将模型包装成一对多的二元分类器。
def svm():
    svm_clf = OneVsRestClassifier(SVC())
    pass


# 随机森林
def rf():
    # 训练500颗决策树，最大深度为16
    forest_clf = RandomForestClassifier(
        n_estimators=500, max_depth=16, random_state=42, n_jobs=-1)

    # 交叉验证结果如下
    # [0.92420611 0.94269427 0.96455392]
    # 0.9438181003223488


# 集成模型
# 将多个模型集成为一个模型，以此预测出的结果是综合了子模型的结果得出的，这种方法有助于提高正确性。
def vot():
    kn_clf = KNeighborsClassifier(n_neighbors=4, weights='distance', n_jobs=-1)
    svm_clf = SVC(probability=True)
    forest_clf = RandomForestClassifier(
        n_estimators=500, max_depth=16, random_state=42, n_jobs=-1)

    voting = VotingClassifier(estimators=[('knn', kn_clf),
                                          ('svm', svm_clf),
                                          ('tree', forest_clf)], voting='soft')

    x_temp = X_train.reshape(X_train.size(0), -1)
    voting.fit(x_temp, y_train)

# vot()


# 评估模型
# 通常训练好的模型在投入生产使用之前，需要对其性能做评估，虽然我们的模型通过交叉验证得到了不错的结果，
# 但模型很可能过拟合，还有一种评估分类器性能的好方法是混淆矩阵

# 混淆矩阵
# 该方法的思路是检查A类别被判断为B类别的次数，若次数过高，则需要采取措施降低混淆的次数。
# 为了建立混淆矩阵，首先需要一组预测数据，然后调用confusion_matrix()建立混淆矩阵
def cmatrix():
    """
    混淆矩阵 - ok
    """
    kn_clf = KNeighborsClassifier(n_neighbors=4, weights='distance', n_jobs=-1)
    # 得到一组预测值
    x_temp = X_train.reshape(X_train.size(0), -1)
    y_temp = y_train
    y_predict = cross_val_predict(kn_clf, x_temp, y_temp, cv=3)
    # 建立混淆矩阵
    matrix = confusion_matrix(y_train, y_predict)
    print(matrix)
    # [[5884    3    4    0    1    4   17    1    5    4]
    #  [1 6706   10    1    3    1    3   10    3    4]
    #  [41   47 5716   18   10    5    9   83   18   11]
    #  [7   12   29 5891    1   72    6   38   49   26]
    #  [3   50    1    1 5636    0   15   16    3  117]
    #  [15    8    4   61    6 5218   56    6   17   30]
    #  [21   11    1    0    7   22 5853    0    3    0]
    #  [3   58   12    2   11    2    0 6109    4   64]
    #  [17   68    8   68   29   81   26   13 5477   64]
    #  [13    9    5   33   51   13    5   76   13 5731]]
    pass

# cmatrix()


# 混淆矩阵输出的是所有类别两两之间判断正确与错误的个数，用MNIST的测试集计算出的混淆矩阵做个示例：
# [[5884    3    4    0    1    4   17    1    5    4]
#  [1 6706   10    1    3    1    3   10    3    4]
#  [41   47 5716   18   10    5    9   83   18   11]
#  [7   12   29 5891    1   72    6   38   49   26]
#  [3   50    1    1 5636    0   15   16    3  117]
#  [15    8    4   61    6 5218   56    6   17   30]
#  [21   11    1    0    7   22 5853    0    3    0]
#  [3   58   12    2   11    2    0 6109    4   64]
#  [17   68    8   68   29   81   26   13 5477   64]
#  [13    9    5   33   51   13    5   76   13 5731]]

# 可以看到，这是一个10 * 10的矩阵，1-10列代表数字0-9，
# 第一行表示数字0被正确分类的个数为5884，被误分类成数字1的个数是3，被误分类成数字2的个数是4，以此类推…


# 测试集
# 当模型在交叉验证与混淆矩阵表现都不错的时候，可以用上测试数据集了，
# 测试集对于模型来说是陌生的，若模型过拟合训练数据，则很容易在测试集上表现差。

# 计算模型在测试集上的准确性
# predict = model.predict(X_test)
# correct = sum(predict == y_test)
# print(correct / len(y_test))

# 结语
# 机器学习的过程，需要不停地探索，从数据预处理，到模型优化，甚至是花更多精力在收集整理数据集上，需要不断通过结果反馈来修正训练过程。
# 有经验的大佬们，应该会花更少的时间就能训练出很好的模型，训练一个模型的成本还是比较高的，即又花费硬件资源，又花费时间。
